fix(acp): spawn prompt turn to prevent permission-gate deadlock - #6660
Merged
Conversation
handle_prompt awaited the entire agent turn inline inside the ACP SDK's on_receive_request callback, holding the SDK's strictly serial dispatch loop for the whole turn. Since that same loop demultiplexes inbound RPC responses, the IDE's reply to a permission-gated tool call's session/request_permission request could never route back while the loop was blocked on the still-running turn, deadlocking indefinitely. Spawn the turn via cx.spawn instead, per the SDK's documented ordering contract, and respond to the session/prompt request from inside the spawned task. This frees the dispatch loop to keep routing inbound messages, including the permission reply, while the turn runs. The spawned task swallows response-send failures instead of propagating them, since an Err returned from a spawned task tears down the entire connection per the SDK's contract, and a failed response send is a symptom of an already-closing connection, not a defect in this turn. Adds integration test coverage for both the approve and deny paths of a permission-gated prompt round trip, guarded by a timeout so a regression fails fast instead of hanging the suite.
bug-ops
enabled auto-merge (squash)
July 27, 2026 21:26
bug-ops
added a commit
that referenced
this pull request
Aug 16, 2026
* docs(readme): sync crate READMEs with commits since v0.22.3 Reconciles all 24 changed crate READMEs against the actual shipped implementation for the v0.22.3..HEAD range: new subsystems (risk-chain detection, capability scoping, plugin dependency graph, session spawn cap), several pre-existing factual errors unrelated to this release (inverted file-sandbox precedence, fabricated MCP config keys, wrong anomaly-detector defaults, stale trust-level names), and terminology/ API renames that had drifted out of sync with the code. * docs(specs): reconcile spec drift for commits since v0.22.3 Closes drift left after the skill-quarantine trust fixes (#6701, #6702, #6706, #6707, #6713), the subagent session-wide spawn cap (#6545), four post-ACP-2.0.0-migration bugfixes (#6660, #6665, #6672, #6684), the mention-picker and TUI interrupt-hint updates, the MAX_RETRY_SECS compile-time bound, the sanitizer secret-shape masking extension, the tracing-guard-flush invariants, and the VigilGate per-process pattern-compile fix. Updates specs/README.md's index to match. * docs(book): sync user docs with commits since v0.22.3 Updates the TUI keybindings and mention-picker pages for the new Ctrl+C semantics, the inline @ mention picker, and the input separator's busy indicator; documents the new [tools.shell] risk_chain_window_turns config key; corrects the ACP protocol version reference (was stale at 0.11.1); bumps the sub-agent frontmatter breaking-change note to v0.22.4. * fix(serve): give build_combined_deps_wires_policy_gate test a dedicated stack cargo nextest run --features full could crash with a stack overflow (SIGABRT) on serve::agent_factory::tests::build_combined_deps_wires_policy_gate_through_to_session_agent. Same defect class already fixed once in this file for issue #6699: building a full Agent under --features full's unboxed AnyProvider variants (Candle/Gonka/Cocoon) reaches the same VigilGate::try_new stack depth that overflows the default 2 MiB test-thread stack in an unoptimized build. The #6699 fix only wrapped the one test it was filed against, leaving this one - added in PR #6007, unrelated to any change in this release - unprotected. CI's test job never caught it because it runs the curated feature set, not full, so the deeper AnyProvider frames never materialize there. Runs the test body on a dedicated 32 MiB-stack thread instead of directly under #[tokio::test], reusing the existing TEST_THREAD_STACK_SIZE constant. * release: prepare v0.22.4 Bump version across the workspace, finalize the CHANGELOG.md [0.22.4] section, refresh the README tests badge, and re-accept the splash-screen snapshots (embed the version string).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
handle_promptawaited the entire agent turn inline inside the ACP SDK'son_receive_requestcallback, holding the SDK's strictly serial dispatch loop for the whole turn. Since that same loop demultiplexes inbound RPC responses, the IDE's reply to a permission-gated tool call'ssession/request_permissionrequest could never route back while the loop was blocked — deadlocking indefinitely (fail-closed, not a permission bypass).cx.spawn(...), responding to thesession/promptRPC request from the spawned task instead of the handler, per the SDK's documented ordering contract. This frees the dispatch loop to keep routing inbound messages, including the permission reply, while the turn runs.send_request(...).block_task().awaitfrom a per-session spawned task (fs.rs,terminal.rs,lsp/acp_provider.rs,agent/elicitation.rs) shared the same deadlock class and is fixed at the root by this change, though only thepermission.rspath is regression-tested directly.spawn_cancel_request_bridgeconvention in the same file): arespond/respond_with_errorfailure is logged and swallowed rather than propagated, since anErrreturned from a spawned task tears down the entire connection per the SDK's contract, and a failed response send is a symptom of an already-closing connection, not a defect in the completed turn.Known behavior change
"prompt already in progress"was previously unreachable from ACP because the blocked dispatch loop serializedsession/promptdispatch. It is now reachable if a client pipelines two prompts on the same session before the first completes — such a client now gets aninternal_errorinstead of implicit queueing. Spec-conformant clients don't do this.Follow-up (not in this PR)
do_promptrestoresoutput_rxonly on its success path; a pre-existing (not introduced here)input_tx.sendfailure early-return leaks it, permanently wedging the session. This is now slightly more observable given the behavior change above. Will file a dedicated follow-up issue.Closes #6656
Test plan
permission_gated_prompt_round_trip_does_not_deadlock(approve path) andpermission_gated_prompt_denial_does_not_deadlock(deny path, fail-closed) integration tests, both timeout-guarded so a regression fails fast.prompt.rs, both tests failed withElapsed(()), restored the fix.cargo +nightly fmt --checkcleancargo clippy --profile ci --workspace --all-targets --features "desktop,ide,server,chat,pdf,scheduler,testing" -- -D warningscleancargo nextest run --config-file .github/nextest.toml --workspace --features "desktop,ide,server,chat,pdf,scheduler" --lib --bins— 15098 passed, 0 failed, 36 skippedRUSTFLAGS="-D warnings" RUSTDOCFLAGS="--deny rustdoc::broken_intra_doc_links" cargo doc --no-deps --workspace) cleangitleaks protect --stagedclean